Go 的 HTTP 标准库-服务端-工作原理
http 标准库
无需框架,Go 语言本身就提供了 http 标准库,可以非常方便地搭建 HTTP 服务端和客户端。
net/http 是 Go 语言中原生的 http 实现,可以提供 http 服务器的功能,其中默认的 DefaultServeMux 提供了基础的路有功能。 net/http 提供了良好的抽象:Server,Listener,Conn,HandlerFunc,Handler 定义了一整套 http 请求的处理流程。
http 库的执行流程
客户端发起的 HTTP 请求是通过 Go 语言实现的 HTTP 服务器监听、接收、处理并返回响应的,这个 HTTP 服务器底层工作流程如下:
- 创建 Listen Socket,监听指定的端口,等待客户端请求到来;
- Listen Socket 接收客户端的请求,得到 Client Socket,接下来通过 Client Socket 与客户端通信;
- 处理客户端的请求,首先从 Client Socket 读取 HTTP 请求的协议头, 如果是 POST 方法, 还可能要读取客户端提交的数据,然后交给相应的 Handler(处理器)处理请求,Handler 处理完毕后装载好客户端需要的数据,最后通过 Client Socket 返回给客户端。
创建 Listen Socket 监听端口
err := http.ListenAndServe(":8000", nil)
该方法底层调用的是 net/http
包的 ListenAndServe 方法,首先会初始化一个 Server 对象
然后调用该 Server 实例的 ListenAndServe 方法,进而调用 net.Listen("tcp", addr)
,也就是基于 TCP 协议创建 Listen Socket,并在传入的 IP 地址和端口号上监听请求,在本例中,IP 地址为空,默认是本机地址,端口号是 8000:
接收客户端请求并建立连接
创建 Listen Socket 成功后,调用 Server 实例的 Serve(net.Listener)
方法,用来接收并处理客户端的请求信息。
这个方法里面起了一个 for 循环,在循环体中首先通过 net.Listener
(即上一步监听端口中创建的 Listen Socket)实例的 Accept
方法接收客户端请求,接收到请求后根据请求信息创建一个 conn 连接实例,最后单独开了一个 goroutine,把这个请求的数据当做参数扔给这个 conn 去服务:
// 上面那张图最后调用的就是这个函数
func (srv *Server) Serve(l net.Listener) error {
...
for {
rw, err := l.Accept()
if err != nil {
....
}
connCtx := ctx
if cc := srv.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
}
这个就是高并发体现了,用户的每一次请求都是在一个新的 goroutine 去服务,相互不影响。客户端请求的具体处理逻辑都是在 c.serve 中完成的。
处理客户端请求并返回响应
err := http.ListenAndServe(":8000", nil)
接下来,可以进入 conn 实例的 serve 方法源码,看看底层如何将 HTTP 请求分配给指定处理器方法进行处理。(代码太长就不贴了)
总之 conn 首先会通过 c.readRequest()
解析请求,然后在 serverHandler{c.server}.ServeHTTP(w, w.req)
的 ServeHTTP 方法(如下代码)中获取相应的 handler := sh.srv.Handler
,也就是在调用函数 ListenAndServe 时候的第二个参数。
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
if req.URL != nil && strings.Contains(req.URL.RawQuery, ";") {
var allowQuerySemicolonsInUse int32
req = req.WithContext(context.WithValue(req.Context(), silenceSemWarnContextKey, func() {
atomic.StoreInt32(&allowQuerySemicolonsInUse, 1)
}))
defer func() {
if atomic.LoadInt32(&allowQuerySemicolonsInUse) == 0 {
sh.srv.logf("http: URL query contains semicolon, which is no longer a supported separator; parts of the query may be stripped when parsed; see golang.org/issue/25192")
}
}()
}
handler.ServeHTTP(rw, req)
}
因为上面传的是 nil
,则默认会获取 DefaultServeMux
,这个 handler 变量其实就是一个路由器,它用来匹配 URL 路由与对应的处理函数,而这个映射关系在 main 函数的第一行代码中就完成了:
http.HandleFunc("/", sayHelloWorld)
其作用就是注册了请求 /
的路由规则,当请求 URL 路由为 /
,就会跳转到函数 sayhelloWorld
来处理请求,DefaultServeMux 会调用 ServeHTTP 方法,这个方法内部其实就是调用 sayhelloWorld 方法本身
可以看这个 HandleFunc 方法源码
在 Go 语言中函数本身是第一类公民,可以当作实现了 Handler 接口的类型,只不过对应的的 ServeHTTP 方法内部调用的是函数自身而已
最后通过写入 ResponseWriter 对象将响应返回到客户端
DefaultServeMux 底层实现
上面说到
http.HandleFunc("/", sayHelloWorld)
err := http.ListenAndServe(":9091", nil)
实际上调用的是内部的 DefaultServeMux
,这个 handler,如果我们想要实现自定义的路由处理器,则需要构建一个自定义的、实现了 Handler 接口的类实例作为 http.ListenAndServe
的第二个参数传入。
在开始介绍自定义路由处理器实现之前,我们先来看看 DefaultServeMux
是如何保存路由映射规则以及分发请求做路由匹配的。
顾名思义,DefaultServeMux 是 ServeMux 的默认实例:
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
这里的后缀 Mux 是 Multiplexer 的缩写,ServeMux 可以看作是 HTTP 请求的多路复用器,它们要实现的功能是:接受 HTTP 请求,然后基于映射规则将其转发给正确的处理器进行处理。
那么在 Go Web 应用中,这些路由映射规则是怎么定义的呢?
首先我们来看一下 ServeMux 的数据结构:
type ServeMux struct {
mu sync.RWMutex // 由于请求涉及到并发处理,因此这里需要一个锁机制
m map[string]muxEntry // 路由规则字典,存放 URL 路径与处理器的映射关系
es []muxEntry // MuxEntry 切片(按照最长到最短排序)
hosts bool // 路由规则中是否包含 host 信息
}
这里,我们需要重点关注的是 muxEntry 结构:
type muxEntry struct {
h Handler // 处理器具体实现
pattern string // 模式匹配字符串
}
最后我们来看一下 Handler 的定义,这是一个接口:
type Handler interface {
ServeHTTP(ResponseWriter, *Request) // 路由处理实现方法
}
当请求路径与 pattern 匹配时,就会调用 Handler 的 ServeHTTP 方法来处理请求。
以我们之前编写的示例应用为例,就是将 URL 路径为 /
的请求转发到 sayHelloWorld 进行处理:
http.HandleFunc("/", sayHelloWorld)
不过 sayHelloWorld 只是一个函数,并没有实现 Handler 接口,之所以可以成功添加到路由映射规则,是因为在底层通过 HandlerFunc()
函数将其强制转化为了 HandlerFunc 类型,而 HandlerFunc 类型实现了 ServeHTTP 方法,这样,sayHelloWorld 方法也就变相实现了 Handler 接口:
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
// 这里实际上是将其路由映射规则保存到 DefaultServeMux 路由处理器的数据结构中(具体看下面)
mux.Handle(pattern, HandlerFunc(handler))
}
...
type HandlerFunc func(ResponseWriter, *Request)
// HandlerFunc 结构的方法
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
对于 sayHelloWorld 方法来说,它已然变成了 HandlerFunc 类型的函数类型,当我们在其实例上调用 ServeHTTP 方法时,调用的是 sayHelloWorld 方法本身。
前面我们提到,DefaultServeMux 是 ServeMux 的默认实例,当我们在 HandleFunc 中调用 mux.Handle
方法时,实际上是将其路由映射规则保存到 DefaultServeMux 路由处理器的数据结构中:
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
if pattern == "" {
panic("http: invalid pattern")
}
if handler == nil {
panic("http: nil handler")
}
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
if pattern[0] != '/' {
mux.hosts = true
}
}
保存好路由映射规则之后,客户端请求又是怎么分发的呢?或者说请求 URL 与 DefaultServeMux 中保存的路由映射规则是如何匹配的呢?
上面说了处理客户端请求时,会调用默认 ServeMux 实现的 ServeHTTP 方法:
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
w.Header().Set("Connection", "close")
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
如上所示,路由处理器接收到请求之后,如果 URL 路径是 *
,则关闭连接,否则调用 mux.Handler(r)
返回对应请求路径匹配的处理器,然后执行 h.ServeHTTP(w, r)
,也就是调用对应路由 handler 的 ServerHTTP 方法,以 /
路由为例,调用的就是 sayHelloWorld
函数本身。
通过上面的介绍,我们了解了基于 DefaultServeMux 实现的整个路由规则存储(Web 应用启动期间进行)和请求匹配过程(客户端发起请求时进行),下面我们来看一下如何实现自定义的 路由处理器。
补充:自定义路由处理器
如果你搞清楚了上面的默认实现,编写自定义的路由处理器就会非常简单,我们只需要定义一个实现了 Handler 接口的类,然后将其实例传递给 http.ListenAndServe
方法即可:
package main
import (
"fmt"
"net/http"
)
type MyHander struct {
}
func (handler *MyHander) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
sayHelloGolang(w, r)
return
}
http.NotFound(w, r)
return
}
func sayHelloGolang(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello Golang!")
}
func main() {
handler := MyHander{}
http.ListenAndServe(":9091", &handler)
}
然后在浏览器中就可以访问 /
路由了:
这个实现很简单,而且我们并没有在应用启动期间初始化路由映射规则,而是在应用启动之后根据请求参数动态判断来做分发的,这样做会影响性能,而且非常不灵活,我们可以通过定义多个处理器的方式来解决这个问题:
package main
import (
"fmt"
"net/http"
)
type HelloHander struct {
}
func (handler *HelloHander) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sayHelloGolang(w, r)
}
func sayHelloGolang(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello Golang!")
}
type WorldHander struct {
}
func (handler *WorldHander) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
}
func main() {
hello := HelloHander{}
world := WorldHander{}
server := http.Server{
Addr: ":9091",
}
http.Handle("/hello", &hello)
http.Handle("/world", &world)
server.ListenAndServe()
}
只是,我们又回到了老路子上,这里没有显式传入 handler,所以底层依然使用的是 DefaultServeMux 那套路由映射与请求分发机制,要实现完全自定义的、功能更加强大的处理器,只能通过自定义 ServeMux 来实现了
整个过程快速整理
如下代码
package main
import (
"net/http"
)
func SayHello(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello"))
}
func main() {
http.HandleFunc("/hello", SayHello)
http.ListenAndServe(":8001", nil)
}
Note:之所以这里 Response、Request 都是指针是因为在应用代码中需要设置响应头和响应实体,所以响应对象理应是指针类型。(这里 ResponseWriter 底层还是指针,下面会讲)
首先调用 Http.HandleFunc
按顺序做了几件事:
- 调用了 DefaultServerMux 的 HandleFunc
- 调用了 DefaultServerMux 的 Handle
- 往 DefaultServeMux 的
map[string]muxEntry
中增加对应的 handler 和路由规则
其次调用 http.ListenAndServe(":8001", nil)
按顺序做了几件事情:
- 实例化 Server
- 调用 Server 的
ListenAndServe()
- 调用
net.Listen("tcp", addr)
监听端口 - 启动一个 for 循环,在循环体中 Accept 请求
- 对每个请求实例化一个 Conn,并且开启一个 goroutine 为这个请求进行服务
go c.serve()
- 读取每个请求的内容
w, err := c.readRequest()
- 判断 header 是否为空,如果没有设置 handler(这个例子就没有设置 handler),handler 就设置为 DefaultServeMux
- 调用 handler 的 ServeHttp
- 在这个例子中,下面就进入到
DefaultServerMux.ServeHttp
- 根据 request 选择 handler,并且进入到这个 handler 的 ServeHTTP
mux.handler(r).ServeHTTP(w, r)
选择 handler 的过程:
A 判断是否有路由能满足这个request(循环遍历ServerMux的muxEntry)
B 如果有路由满足,调用这个路由handler的ServeHttp
C 如果没有路由满足,调用NotFoundHandler的ServeHttp
net/http 和 gin 的关系
gin 更像是一个功能强大的路由器,提供更便捷的 web 服务解决方案,而其余功能则复用 net/http。 网络层实现,http parser 都是由 net/http 实现的。
两者的关系见下图所示
先来看下 net/http 的缺点
- 请求响应编解码繁琐
- 默认的 mutex 性能问题
- 时间复杂度: + 正则匹配
- 没有中间件、监控支持
- 不太好的内存管理
- request/response 无法复用(请求级别)
- 无条件的解析请求头
gin 对这些点的改进
- 实现了
http.Handler
接口的轻量级框架 - 提供了高性能的路由:Radix Tree 实现(前缀树)
- 提供工具简化了输入输出处理:binding 处理
- 提供了中间件的支持
- 提供 web 服务的常用工具函数,如 panic 捕获,json 格式校验等
- 使用 context 池,减少 runtime 的 GC 工作量。
- 强大的工具包:
gin.Context
gin.Context 提供了一系列解析、校验请求的方法,其中内置了 validator 参数校验